package com.mrx.xfriendship.service;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.mrx.commons.annotations.XLog;
import com.mrx.commons.util.JWTUtil;
import com.mrx.commons.util.asserts.Asserts;
import com.mrx.xfriendship.converter.PhotoWallConverter;
import com.mrx.xfriendship.converter.UserConverter;
import com.mrx.xfriendship.converter.UserDetailConverter;
import com.mrx.xfriendship.converter.UserManagerDTOConverter;
import com.mrx.xfriendship.dao.*;
import com.mrx.xfriendship.model.bo.RecoveryBO;
import com.mrx.xfriendship.model.dto.*;
import com.mrx.xfriendship.model.po.PhotoWallPO;
import com.mrx.xfriendship.model.po.UserDetailPO;
import com.mrx.xfriendship.model.po.UserPO;
import com.mrx.xfriendship.model.vo.*;
import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.code.DefaultCodeGenerator;
import dev.samstevens.totp.code.DefaultCodeVerifier;
import dev.samstevens.totp.code.HashingAlgorithm;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.util.Utils;
import lombok.SneakyThrows;
import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.lang.NonNull;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author Mr.X
 * @since 2023-01-14 12:07
 */
@Service
@Validated
@DubboService
@XLog(ignoreMethod = "heartbeat")
public class UserServiceImpl implements IUserService {

    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    @Resource
    private UserMapper userMapper;

    @Resource
    private UserDetailMapper userDetailMapper;

    @Resource
    private UserConverter userConverter;

    @Resource
    private UserDetailConverter userDetailConverter;

    @Resource
    private PasswordEncoder encoder;

    @Resource
    private IRedisService redisService;

    @Resource
    private ICaptchaService captchaService;

    @Resource
    private IPSearcherService ipSearcherService;

    @Resource
    private UserManagerDTOConverter userManagerDTOConverter;

    @Resource
    private RoleMapper roleMapper;

    @Resource
    private PhotoWallMapper photoWallMapper;

    @Resource
    private PhotoWallConverter photoWallConverter;

    @Resource
    private SystemInfoMapper systemInfoMapper;

    @Resource
    private IMinioService minioService;

    @Resource
    private PostMapper postMapper;

    @NonNull
    @Override
    @Cacheable(value = "cache:securityUser", key = "#name")
    public SecurityUserDTO loadSecurityUserByName(String name) {
        return userConverter.toSecurityUserDTO(userMapper.selectByName(name));
    }

    @Override
    public UserDTO selectUserByName(@NonNull String name) {
        return userConverter.toDTO(userMapper.selectByName(name));
    }

    @Override
    public UserDTO login(String t, String captcha, LoginUserVO userVO) {
        // 检查 验证码
        Asserts.CAPTCHA_ERROR.checkTrue(captchaService.verifyCaptcha(t, captcha));
        // 获取数据库中的用户
        UserPO userPO = Asserts.USER_NOT_EXISTS.checkNotNull(userMapper.selectByName(userVO.getName()));
        // 判断密码是否正确
        Asserts.LOGIN_FAILURE.checkTrue(encoder.matches(userVO.getPass(), userPO.getPass()));
        // 标记此用户已上线
        heartbeat(userPO.getId());
        // 如果登录成功, 就刷新 token
        return refreshToken(userPO);
    }

    @Override
    public void logout(String userName) {
        redisService.removeUserToken(userName);
        // 移除 该用户的在线信息
        redisService.minusOnlineUser(selectUserByName(userName).getId());
    }

    @Override
    @CacheEvict(value = "cache:securityUser")
    @Transactional(rollbackFor = Exception.class)
    public void register(String t, String captcha, String ip, RegisterUserVO vo) {
        // 检查 验证码
        Asserts.CAPTCHA_ERROR.checkTrue(captchaService.verifyCaptcha(t, captcha));
        // 获取数据库中的用户, 由于有 唯一索引, 所以这里需要判断 用户不存在
        Asserts.USER_EXISTS.checkNull(userMapper.selectByName(vo.getName()));
        UserPO userPO = userConverter.toPO(vo);
        logger.debug("register: {}", userPO);
        // 插入 user 表
        userMapper.insertSelective(userPO);
        // 插入 userDetails 表
        UserDetailPO record = userConverter.toUserDetail(userPO);
        // 设置注册 ip
        record.setIpLocation(ipSearcherService.search(ip));
        userDetailMapper.insertSelective(record);
    }

    @Override
    public UserInfoDTO userInfo(Integer userId) {
        UserDetailPO userDetailPO = userDetailMapper.selectByUserId(userId);
        return userDetailConverter.toUserInfo(userDetailPO);
    }

    @Override
    public String updateAvatar(Integer userId, String bucketFileName) {
        userDetailMapper.updateAvatarByUserId(userId, bucketFileName);
        return bucketFileName;
    }

    @Override
    public void updateInfo(Integer userId, UserInfoVO info) {
        userDetailMapper.updateByPrimaryKeySelective(userDetailConverter.toUserDetail(userId, info));
    }

    @Override
    public void updatePassword(Integer userId, UserInfoVO info) {
        UserPO userPO = userConverter.toPO(userId, info);
        // 判断旧密码是否正确
        Asserts.PASSWORD_ERROR.checkTrue(encoder.matches(info.getOldPassword(), userMapper.selectByPrimaryKey(userPO.getId()).getPass()));
        userMapper.updateByPrimaryKeySelective(userPO);
    }

    @Override
    public UserDTO verify2faCode(String userName, String code) {
        // 获取数据库中的用户
        UserPO userPO = Asserts.USER_NOT_EXISTS.checkNotNull(userMapper.selectByName(userName));
        // 校验 2fa 密码
        CodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), new SystemTimeProvider());
        Asserts.TWO_FA_CODE_ERROR.checkTrue(verifier.isValidCode(getSecretOfTime(userPO.getCreateTime()), code));
        return refreshToken(userPO);
    }

    @Override
    @SneakyThrows
    public String req2faCode(String userName, Integer userId) {
        QrData data = new QrData.Builder()
                .label(userName)
                .secret(getSecretOfTime(userMapper.selectByPrimaryKey(userId).getCreateTime()))
                .issuer("华夏交友圈")
                .algorithm(HashingAlgorithm.SHA1)
                .digits(6)
                .period(30)
                .build();
        ZxingPngQrGenerator generator = new ZxingPngQrGenerator();
        return Utils.getDataUriForImage(generator.generate(data), generator.getImageMimeType());
    }

    private String getSecretOfTime(LocalDateTime time) {
        return JWTUtil.SECRET + time.toEpochSecond(ZoneOffset.ofHours(8));
    }

    private UserDTO refreshToken(UserPO userPO) {
        String userName = userPO.getName();
        String token = JWTUtil.getToken(userName);
        redisService.setUserToken(userName, token);
        UserDTO dto = userConverter.toDTO(userPO);
        dto.setToken(token);
        return dto;
    }

    @Override
    public String getAvatar(Integer id) {
        return userDetailMapper.selectByUserId(id).getAvatar();
    }

    @Override
    public void signing(Integer id) {
        UserDetailPO detailPO = userDetailMapper.selectByUserId(id);
        LocalDateTime lastSignTime = detailPO.getLastSignTime();
        Asserts.SIGNING_FAILURE.checkTrue(lastSignTime == null || lastSignTime.until(LocalDateTime.now(), ChronoUnit.HOURS) >= 24);
        detailPO.setScore(detailPO.getScore() + 10);
        detailPO.setLastSignTime(LocalDateTime.now());
        userDetailMapper.updateByPrimaryKeySelective(detailPO);
    }

    @Override
    public PageDTO<UserManagerDTO> listUsers(UserManagerPageVO vo) {
        try (Page<?> page = PageHelper.startPage(vo.getPage(), vo.getSize())) {
            return PageDTO.newPage(vo, userManagerDTOConverter.toDTO(userMapper.listUsers(vo)), page.getTotal());
        }
    }

    @Override
    @CacheEvict(value = "cache:securityUser")
    public void updateUser(UpdateUserVO vo) {
        UserDetailPO record = userDetailConverter.toPOForUpdate(vo);
        record.setRoleId(roleMapper.selectByRole(vo.getRole()).getId());
        userDetailMapper.updateByPrimaryKeySelective(record);
    }

    @Override
    @CacheEvict(value = "cache:securityUser")
    public void deleteUser(Integer id) {
        // 删除 user 表
        userMapper.deleteByPrimaryKey(id);
        // 删除 userDetail 表
        userDetailMapper.deleteByUserId(id);
    }

    @Override
    public String updatePhotoWall(Integer currentUserId, String upload) {
        PhotoWallPO wallPO = new PhotoWallPO();
        wallPO.setUserid(currentUserId);
        wallPO.setUrl(upload);
        wallPO.setDesc("图片描述");
        photoWallMapper.insertSelective(wallPO);
        return upload;
    }

    @Override
    public void deletePhotoWall(Integer currentUserId, Integer photoId) {
        PhotoWallPO po = photoWallMapper.selectByPrimaryKey(photoId);
        Asserts.PHOTO_NOT_EXIST.checkNotNull(po);
        Asserts.CAN_ONLY_DELETE_YOURSELF_PHOTO.checkTrue(currentUserId.equals(po.getUserid()));
        photoWallMapper.deleteByPrimaryKey(photoId);
    }

    @Override
    public void updatePhotoDesc(Integer currentUserId, PhotoWallVO vo) {
        PhotoWallPO po = photoWallMapper.selectByPrimaryKey(vo.getId());
        Asserts.PHOTO_NOT_EXIST.checkNotNull(po);
        Asserts.CAN_ONLY_DELETE_YOURSELF_PHOTO.checkTrue(currentUserId.equals(po.getUserid()));
        photoWallMapper.updateByPrimaryKeySelective(photoWallConverter.toPO(vo));
    }

    @Override
    public void heartbeat(Integer currentUserId) {
        redisService.plusOnlineUser(currentUserId);
    }

    @Override
    public Integer onlineUserNumber() {
        return redisService.getOnlineUser();
    }

    @Override
    public Integer maxOnlineUserNumber() {
        SystemInfoDTO systemInfo = new SystemInfoDTO();
        systemInfo.setKey("maxOnLineUserNum");
        SystemInfoDTO systemInfoDTO = systemInfoMapper.selectSelective(systemInfo);
        // 如果数据库里没有这个字段
        if (systemInfoDTO == null) {
            systemInfo.setValue("0");
            // 那就初始化一下默认值
            systemInfoMapper.insert(systemInfo);
        } else {
            // 否则就使用 数据库里的数据
            systemInfo.setValue(systemInfoDTO.getValue());
        }
        return Integer.parseInt(systemInfo.getValue());
    }

    @Override
    public void syncMaxOnLineUserNumber(Integer onlineUserNumber) {
        SystemInfoDTO systemInfo = new SystemInfoDTO();
        systemInfo.setKey("maxOnLineUserNum");
        SystemInfoDTO systemInfoDTO = systemInfoMapper.selectSelective(systemInfo);
        // 如果数据库里没有这个字段
        if (systemInfoDTO == null) {
            systemInfo.setValue("0");
            // 那就初始化一下默认值
            systemInfoMapper.insert(systemInfo);
        } else {
            // 否则就使用 当前在线用户数据 和 数据库中的 最在线用户数据 中的最大值, 将其放入数据库
            systemInfo.setValue(Math.max(onlineUserNumber, Integer.parseInt(systemInfoDTO.getValue())) + "");
            // 更新下数据
            systemInfoMapper.updateByKey(systemInfo);
        }
    }

    @Override
    public PageDTO<ScoreRankDTO> scoreRank(PageVO pageVO) {
        try (Page<?> page = PageHelper.startPage(pageVO.getPage(), pageVO.getSize())) {
            List<UserDetailPO> list = userDetailMapper.selectList();
            List<UserPO> userPOList = userMapper.selectByIds(list.stream().map(UserDetailPO::getUserId).collect(Collectors.toList()));
            return PageDTO.newPage(pageVO, userDetail2ScoreRankDTO(list, userPOList), page.getTotal());
        }
    }

    @Override
    public ScoreRankDTO myScoreRank(Integer userId) {
        ScoreRankDTO dto = new ScoreRankDTO();
        UserPO userPO = userMapper.selectByPrimaryKey(userId);
        UserDetailPO userDetailPO = userDetailMapper.selectByUserId(userId);
        Integer rank = userDetailMapper.selectRank(userId);

        dto.setId(userId);
        dto.setName(userPO.getName());
        dto.setAvatar(userDetailPO.getAvatar());
        dto.setScore(userDetailPO.getScore());
        dto.setRank(rank);
        return dto;
    }

    @Override
    public PageDTO<PostRankDTO> postRank(PageVO pageVO) {
        try (Page<?> page = PageHelper.startPage(pageVO.getPage(), pageVO.getSize())) {
            List<UserDetailPO> list = userDetailMapper.selectList();
            List<UserPO> userPOList = userMapper.selectByIds(list.stream()
                    .map(UserDetailPO::getUserId)
                    .collect(Collectors.toList())
            );
            List<PostCountDTO> postCountDTOS = postMapper.selectPostCount();
            return PageDTO.newPage(pageVO, userDetail2PostRankDTO(list, userPOList, postCountDTOS), page.getTotal());
        }
    }

    private List<PostRankDTO> userDetail2PostRankDTO(List<UserDetailPO> list, List<UserPO> userPOList, List<PostCountDTO> postCountDTOS) {
        Map<Integer, String> userPOMap = userPOList.stream().collect(Collectors.toMap(UserPO::getId, UserPO::getName));
        Map<Integer, Integer> postInfoMap = postCountDTOS.stream()
                .collect(Collectors.toMap(PostCountDTO::getCreateUserId, PostCountDTO::getNum));
        return list.stream().map(it -> {
            PostRankDTO dto = new PostRankDTO();
            dto.setId(it.getUserId());
            dto.setName(userPOMap.getOrDefault(it.getUserId(), "未知用户"));
            dto.setAvatar(minioService.preview(it.getAvatar()));
            dto.setPostNum(postInfoMap.get(it.getUserId()));
            dto.setRank(null);
            return dto;
        }).collect(Collectors.toList());
    }

    @Override
    public PostRankDTO myPostRank(Integer userId) {
        PostRankDTO dto = new PostRankDTO();
        UserPO userPO = userMapper.selectByPrimaryKey(userId);
        UserDetailPO userDetailPO = userDetailMapper.selectByUserId(userId);
        Integer rank = postMapper.selectRank(userId);
        Integer number = postMapper.selectPostNumByUserId(userId);

        dto.setId(userId);
        dto.setName(userPO.getName());
        dto.setAvatar(userDetailPO.getAvatar());
        dto.setPostNum(number);
        dto.setRank(rank);
        return dto;
    }

    @Override
    public void bindMail(String userName, String code) {
        String keyInRedis = redisService.getBindMail(userName)
                .orElseThrow(Asserts.BIND_MAIL_FAILED_NO_KEY_FOUND::getException);
        // redis 中的数据是 mail:code
        Asserts.BIND_MAIL_FAILED_KEY_ERROR.checkTrue(keyInRedis.endsWith(code));
        // 最后更新 mail 字段
        Asserts.BIND_MAIL_FAILED.checkTrue(userMapper.bindMail(userName, keyInRedis.split(":")[0]) == 1);
    }

    @Override
    public void recovery(RecoveryBO bo) {
        RecoveryVO vo = bo.getRecoveryVO();
        UserPO userPO = userMapper.selectByPrimaryKey(bo.getUserId());
        // 检查 旧密码 是否正确
        Asserts.OLD_PASSWORD_ERROR.checkTrue(encoder.matches(vo.getOldPass(), userPO.getPass()));
        userPO.setPass(encoder.encode(vo.getNewPass()));
        userMapper.updateByPrimaryKeySelective(userPO);
        // 使旧密码 token 失效
        logout(userPO.getName());
    }

    @NonNull
    private List<ScoreRankDTO> userDetail2ScoreRankDTO(@NonNull List<UserDetailPO> userDetailList, @NonNull List<UserPO> userPOList) {
        Map<Integer, String> userPOMap = userPOList.stream().collect(Collectors.toMap(UserPO::getId, UserPO::getName));
        return userDetailList.stream().map(it -> {
            ScoreRankDTO dto = new ScoreRankDTO();
            dto.setId(it.getUserId());
            dto.setName(userPOMap.getOrDefault(it.getUserId(), "未知用户"));
            dto.setAvatar(minioService.preview(it.getAvatar()));
            dto.setScore(it.getScore());
            return dto;
        }).collect(Collectors.toList());
    }

}
